﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using ERF;

namespace FirstPerson {
    /// <summary>
    ///  Provides methods for creating a Terrain from given height data.
    /// </summary>
    static class TerrainGenerator {
        // This constant decides how the dense the texture will be tiled across the terrain.
        private const float UV_SCALE = 1.5f;

        /// <summary>
        /// Creates a new Terrain.
        /// </summary>
        /// <param name="game">The game that the created terrain will belong to.</param>
        /// <param name="input">The height data.</param>
        /// <param name="terrain">Will contain the created terrain when the method call returns.</param>
        /// <param name="sceneGraph">The SceneGraph that contains the entire world. (CURRENTLY NOT USED).</param>
        public static void GenerateTerrain(ERF.ERFGame game, double[,] input, out Terrain terrain, float height, float width, float depth){
            VertexPositionNormalTexture[] terrainVertices;
            //VertexPositionNormalTexture[] waterVertices;
            ushort[] indices;

            // How many vertices per row?
            int stride = input.GetUpperBound(0);

            // How many rows?
            int lines = input.GetUpperBound(1);

            // Calculate the vertical and horizontal spacing.
            float horizontalSpacing = width / stride;
            float verticalSpacing = depth / stride;

            // Generate the terrain.
            TerrainGenerator.CreateTerrainVertices(ref input, stride, lines, horizontalSpacing, verticalSpacing, height, out terrainVertices);
            TerrainGenerator.SetUpIndices(out indices, stride, lines);
            TerrainGenerator.AdjustNormals(ref terrainVertices, ref indices, stride, lines);

            terrain = new Terrain(game, ref terrainVertices, ref indices, -(stride * (horizontalSpacing/ 2)), -(lines * (verticalSpacing / 2)),(stride - 1) * horizontalSpacing, (lines - 1) * verticalSpacing, height, stride, lines, horizontalSpacing, verticalSpacing);          
        }

        /// <summary>
        /// Creates the vertices for the terrain.
        /// </summary>
        /// <param name="input">The height data.</param>
        /// <param name="stride">The number of horizontal vertices.</param>
        /// <param name="lines">The number of vertical vertices.</param>
        /// <param name="vertices">Will contain an array of vertices when the method call returns.</param>
        private static void CreateTerrainVertices(ref double[,] input, int stride, int lines, float horizontalSpacing, float verticalSpacing, float height, out VertexPositionNormalTexture[] vertices) {
            List<VertexPositionNormalTexture> verticeList = new List<VertexPositionNormalTexture>();
            float vertexX, vertexY, vertexZ;

            float xOffset = -(stride * (horizontalSpacing * 0.5f));
            float yOffset = -(lines * (verticalSpacing * 0.5f));

            Vector2 textureCoordinate = Vector2.Zero;
            
            for (int y = 0; y < lines; y++) {
                for (int x = 0; x < stride; x++) {
                    vertexX = xOffset + (x * horizontalSpacing);
                    vertexY = (float)Math.Pow( Math.Max(input[x, y],0), 2) * height;
                    vertexZ = yOffset + (y * verticalSpacing);
                    verticeList.Add(new VertexPositionNormalTexture(
                        new Vector3(vertexX, vertexY, vertexZ),
                        Vector3.Up,
                        new Vector2(x * UV_SCALE, y * UV_SCALE)));
                }
            }

            vertices = verticeList.ToArray();
        }

        /// <summary>
        /// Creates the set of indices needed for a given terrain size.
        /// </summary>
        /// <param name="indices">Will contain an array of ushort indices when the method call returns.</param>
        /// <param name="terrainWidth">The vertice width of the terrain.</param>
        /// <param name="terrainHeight">The vertice height of the terrain.</param>
        private static void SetUpIndices(out ushort[] indices, int terrainWidth, int terrainHeight) {
            indices = new ushort[(terrainWidth - 1) * (terrainHeight - 1) * 6];
            int counter = 0;
            for (int y = 0; y < terrainHeight - 1; y++) {
                for (int x = 0; x < terrainWidth - 1; x++) {
                    ushort lowerLeft = (ushort)(x + y * terrainWidth);
                    ushort lowerRight = (ushort)((x + 1) + y * terrainWidth);
                    ushort topLeft = (ushort)(x + (y + 1) * terrainWidth);
                    ushort topRight = (ushort)((x + 1) + (y + 1) * terrainWidth);

                    indices[counter++] = topLeft;
                    indices[counter++] = lowerLeft;
                    indices[counter++] = lowerRight;

                    indices[counter++] = topLeft;
                    indices[counter++] = lowerRight;
                    indices[counter++] = topRight;
                }
            }
        }

        /// <summary>
        /// Adjusts the normal of each vertex based on its neighbor vertices.
        /// </summary>
        /// <param name="vertices">The array of vertices to adjust the normals on.</param>
        /// <param name="indices">The indices for the vertices.</param>
        /// <param name="stride">The number of horizontal lines.</param>
        /// <param name="lines">The number of vertical lines.</param>
        private static void AdjustNormals(ref VertexPositionNormalTexture[] vertices, ref ushort[] indices, int stride, int lines)
        {
            for (uint i = 0; i < vertices.Length; i++)
                vertices[i].Normal = new Vector3(0, 0, 0);

            // For each triangle in the mesh, we just find two sides of it and calculate the cross product of these two sides.
            // The normal of each vertex in the triangle will have the cross product result added to it.
            for (uint i = 0; i < indices.Length / 3; i++)
            {
                int index1 = indices[i * 3];
                int index2 = indices[i * 3 + 1];
                int index3 = indices[i * 3 + 2];

                Vector3 side1 = (vertices[index1].Position - vertices[index3].Position);
                Vector3 side2 = (vertices[index1].Position - vertices[index2].Position);
                Vector3 normal = Vector3.Cross(side1, side2);

                vertices[index1].Normal += normal;
                vertices[index2].Normal += normal;
                vertices[index3].Normal += normal;
            }

            for (uint i = 0; i < vertices.Length; i++)
                vertices[i].Normal.Normalize();
        }
    }

}
